iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

1. 前言:資料明明都在 BigQuery,模型卻偏偏要靠幻覺回答問題??

到 Day 20 為止,每一次呼叫 Gemini 都是我們先把資料查好、整理成文字或圖片再交給它,Day 09 交的是異常摘要,Day 16 交的是廣告圖,模型只負責讀和寫,要看哪些資料是我們事先決定的,這種做法在題目固定的時候很好用,可是同事隨口問一句「上個月 TikTok 廣告花了多少錢」,沒有人事先替模型準備好這一題的資料。

我把這個問題直接丟給 gemini-3.6-flash,沒有附任何資料,它回答「上個月 TikTok 廣告的總支出為新台幣 125,000 元,整體 ROAS 表現為 3.5」,還補了一句詳細數據已整理在行銷月報資料夾中,這個品牌從頭到尾沒有投過 TikTok,資料庫裡一筆都沒有,125,000 和 3.5 都是它自己編的,而且語氣和真的查過一模一樣,跟它生氣也沒用只能另外想方法。

今天要做的是 Function Calling,把「查資料」這件事寫成工具交給 Gemini,讓它自己判斷這個問題要不要查、查哪一個、參數填什麼,查回來的數字再由它整理成回答。

今日核心目標:

  1. 把 Day 08 的歸因、Day 09 的異常診斷、Day 17 的素材成效各寫成一個查詢工具,學會工具定義的三個部分
  2. 看懂一次工具呼叫的完整來回,模型只提出要求,真正去查資料庫的是我們的程式
  3. 同一批六個問題在有工具和沒有工具兩種情況各問一次,用實測結果對照差別

2. 系統架構全景與設計理念

圖一:使用者的問題和三個工具的定義一起送給 Gemini,模型回傳要呼叫的工具與參數,由程式檢查參數後查 BigQuery,結果交回模型整理成回答

步驟 誰做的 做什麼
1 程式 把問題和三個工具的定義一起送給 Gemini
2 Gemini 不直接回答,回傳「我要呼叫哪個工具、參數是什麼」
3 程式 檢查參數,用寫好的 SQL 查 BigQuery
4 程式 把查詢結果連同模型上一輪的內容一起送回去
5 Gemini 根據查到的資料寫出回答

💡 核心工程理念:

  1. 模型只開口,動手的是程式:Gemini 沒有連上 BigQuery,它回傳的只是一段「請幫我呼叫這個函式」的結構化資料,要不要照做、怎麼做都由我們的程式決定,所有的權限和檢查也都放在程式這一側
  2. SQL 寫死,只開放參數:三個工具各有一段固定的 SQL,模型能決定的只有歸因算法、日期、通路、指標這幾個參數,它寫不出也改不了 SQL,查得到什麼資料在寫工具的時候就定好了
  3. 拿同一批問題做對照:系統指示、問題、模型和溫度都一樣,唯一的差別是有沒有附上工具定義,這樣回答的差異才能算在工具頭上

3. 核心技術深度拆解

3.1 工具定義的三個部分:名稱、說明、參數

一個工具在模型眼中只有三樣東西,下面是查歸因的那一個,為了好讀只節錄一部分,日期和排除直接流量兩種參數沒有列出來:

{
    "name": "get_channel_attribution",
    "description": "查各個流量來源(通路)在一段期間內分到多少訂單功勞與營收,"
                   "用來回答「哪個通路帶來最多訂單」這類問題。"
                   "資料期間是 2026-06-19 到 2026-09-16。",
    "parameters": {
        "type": "OBJECT",
        "properties": {
            "attribution_model": {
                "type": "STRING",
                "enum": ["first_touch", "last_touch", "time_decay"],
                "description": "功勞怎麼分:first_touch 全部算給第一次接觸、"
                               "last_touch 全部算給下單前最後一次接觸、"
                               "time_decay 越接近下單分到越多",
            },
        },
        "required": ["attribution_model"],
    },
}
部分 給模型的訊息 寫的時候要注意
名稱 這個工具叫什麼 用動詞開頭的英文,一看就知道是查什麼
說明 什麼情況該用它 寫出它能回答哪一類問題,以及資料的範圍
參數 可以填什麼 能列舉的就用 enum 列出來,每個選項的意思寫清楚

模型看不到函式裡的程式,也看不到資料表,它決定要不要用這個工具的依據只有說明文字,所以說明其實是寫給模型看的提示,「用來回答哪個通路帶來最多訂單這類問題」這種句子比「歸因查詢」有用得多,參數的 enum 也是同樣的道理,使用者說的是「最終點擊」,模型要能對到 last_touch,靠的就是那一行選項說明。

今天的三個工具:

工具 讀的表 參數
get_channel_attribution mart_attribution(Day 08) 歸因算法、起訖日期、要不要排除直接流量
get_anomaly_diagnosis diag_summary、mart_diagnosis(Day 09) 通路
get_creative_feature_lift mart_creative_lift(Day 17) 指標(點擊率或轉換率)

3.2 一次完整的來回:模型提出要求,程式去查

以「6 月 19 日到 9 月 16 日這段期間,用時間衰減的算法,哪個通路分到的訂單功勞最多」這一題為例,第一次呼叫模型,它沒有回任何文字,回的是這一段:

{"name": "get_channel_attribution",
 "args": {"attribution_model": "time_decay", "start_date": "2026-06-19", "end_date": "2026-09-16"}}

題目裡的「時間衰減」被它對到 time_decay,「6 月 19 日到 9 月 16 日」被它補上年份轉成兩個日期,工具說明裡寫了資料期間是 2026 年,程式收到之後檢查參數、查 BigQuery,把六個通路的結果交回去,第二次呼叫模型才得到回答:

在時間衰減模型下,分到最多訂單功勞的通路是 (direct) / (none)(直接流量),約分得 620 筆訂單。若排除直接流量,付費廣告通路中則以 meta / paid_social 分到的功勞最多,約 597 筆訂單。

620 和 597 都對得上資料表(620.4 與 596.6),所以需要查資料的問題至少要呼叫模型兩次,第一次拿到要求,第二次拿到回答。

用 Google Gen AI SDK 寫這一段有兩個地方要留意,第一是 SDK 預設會自動執行函式,把 Python 函式直接放進 tools 它就會自己呼叫、自己把結果送回去,寫起來最省事,但中間每一步都看不到也記不下來,今天的程式把這個功能關掉,改成自己處理每一輪,第二是把結果送回去的時候,模型上一輪回傳的內容要原封不動放回對話裡,那裡面除了函式呼叫還帶著模型思考過程的簽章,自己重新組一份會少掉這一段,下面是簡化後的寫法:

resp = client.models.generate_content(model=MODEL, contents=contents, config=config)
contents.append(resp.candidates[0].content)          # 模型這一輪的內容原樣放回去
contents.append(types.Content(role="user", parts=[
    types.Part.from_function_response(name=f.name, response=result)
    for f, result in results]))                      # 再接上每個工具查到的結果

3.3 不讓模型自己寫 SQL 的三個原因

另一種做法是只給一個工具叫「執行 SQL」,把資料表結構告訴模型,讓它自己寫查詢,彈性大很多,今天沒有這樣做,原因有三個:

  1. 算法會跑掉:歸因表一列是一筆訂單的一個觸點,功勞要用加總的不能用數列數,這種規則寫在固定的 SQL 裡只要對一次,交給模型每次重寫就每次都可能錯
  2. 範圍守不住:模型寫得出 SELECT,也寫得出掃整張事件表的查詢,或是讀到不該給它看的欄位
  3. 結果沒辦法驗:固定的工具可以事先測好,自由的 SQL 只能事後一筆一筆看

寫死 SQL 之後,程式這一側還有四道檢查,都在 agent/tools.py 裡:

檢查 做法
參數白名單 歸因算法、通路、指標只收列舉裡的值,日期只收 YYYY-MM-DD,多給的參數直接拒絕
不拼接字串 值用查詢參數帶進 SQL,欄位名稱來自程式裡的對照表,不是模型給的字串
掃描量上限 每次查詢最多掃 100 MB,超過就失敗、不收費
回傳列數上限 最多回 20 列,結果越長下一輪的輸入 Token 越多

參數沒通過檢查時程式不會去查 BigQuery,只把錯誤訊息當成工具的結果回給模型,讓它自己修正或是告訴使用者查不到。

3.4 實測:同樣六個問題,有工具和沒有工具

圖二:六個問題在有工具與沒有工具兩種情況的回答對照,有工具時四題的數字與原因都對得上資料表,沒有工具時四題出現資料表裡沒有的內容

六個問題分四種:三題各需要一個工具,一題要用到兩個工具,一題是觀念題不需要查,一題是工具答不了的(就是前言那題 TikTok),模型用 gemini-3.6-flash,溫度設 0,系統指示只有一句「你是電商品牌的行銷資料助理,用繁體中文、三句話以內回答同事的問題」,沒有額外叮嚀它不要亂猜。

有工具的結果:

題目 模型要求的工具與參數 回答
時間衰減下哪個通路功勞最多 歸因,time_decay 直接流量約 620 筆,對
meta 的成效出過哪些狀況 診斷,meta 素材疲乏、競價變貴,另外提到全站的追蹤碼失效,對
有人物的圖點擊率會不會比較高 素材成效,ctr 1.282 倍,對
meta 兩種算法的功勞各多少,加上異常原因 歸因兩次(last_touch、time_decay)加診斷一次 380 筆與 596.6 筆,原因也對
時間衰減是什麼意思 沒有呼叫 直接解釋
上個月 TikTok 廣告花了多少 沒有呼叫 回答沒有 TikTok 的費用資料,無法提供金額

四題需要查資料的都選對工具、參數也對,回答裡的數字和原因全部對得上資料表,第四題值得多看一眼,它在同一輪就一口氣提出三個呼叫,歸因查兩次、診斷查一次,程式依序查完一起交回去,所以這一題和其他題一樣只呼叫了模型兩次,觀念題它判斷不需要查就直接回答,TikTok 那題它回答現有資料只涵蓋 Meta、Google 和 LINE,沒有硬查也沒有編數字。

沒有工具的結果:

題目 回答摘要 和資料表對照
時間衰減下哪個通路功勞最多 說明自己無法存取報表數據,請對方提供資料,另外補了一句通則說最接近下單的點擊通路通常分最多 沒有編數字,但那句通則和資料表不符,實際最多的是直接流量
meta 的成效出過哪些狀況 iOS 14.5 隱私政策、API 異常、演算法調整,最後提到市場競價加劇與素材疲勞 前三項資料表裡沒有,後兩項是泛稱,沒有說是哪個廣告群組、哪一週
有人物的圖點擊率會不會比較高 平均能提升約 20% 到 50% 實際是 1.282 倍,這個範圍是泛稱不是這個品牌的數字
meta 兩種算法的功勞各多少 最終點擊 1,250 筆、時間衰減 980 筆,原因是 Pixel 追蹤碼更新與 iOS 歸因延遲 實際是 380 與 596.6,數字和原因都是編的
時間衰減是什麼意思 正確解釋 不需要資料
上個月 TikTok 廣告花了多少 新台幣 125,000 元,ROAS 3.5 資料庫沒有 TikTok

六題裡只有第一題老實說查不到,另外有四題給了資料表裡沒有的內容,其中兩題編出了看起來很精確的數字,最麻煩的是這些回答讀起來都很合理,1,250 和 980 連大小關係都和真實資料相反,不對照資料表根本看不出來,同一個模型對第一題會說查不到、對第四題卻直接給數字,沒有工具的時候它會不會猜是沒辦法預期的。

這一輪每題每種情況只問一次,換個問法或再問一次,沒有工具那一側的結果可能不一樣,但有工具時數字來自查詢結果、沒有工具時數字只能來自模型自己,這一點不會變。


4. FinOps 成本防護實踐:三道防線體系

  1. 第一道防線:善用 Google Cloud 每月免費額度:三個工具查的都是前幾天建好的小表,今天工具的六次查詢裡五次沒有計費、一次以最小單位 10 MB 計,在 BigQuery 每月 1 TiB 的查詢免費額度內,送出前數輸入 Token 的 count_tokens 也不收費
  2. 第二道防線:架構層被動成本防護:一個問題最多呼叫模型 4 次,每次送出前先數輸入 Token,超過 4,000 就不送,輸出上限 2,048,run.sh 會先用這三個上限算出最壞金額印出來,輸入 yes 才開始,問過而且成功的題目不會再問
  3. 第三道防線:Cloud Billing 預算警報:沿用 Day 03 由 Terraform 建立的預算警報,50%、80%、100% 三段通知,今天的操作不會觸發
情況 題數 呼叫模型 輸入 Token 輸出加思考 Token 費用 換算每 1,000 題
有工具 6 10 次 8,839 1,542 新台幣 0.40 元 新台幣 66 元
沒有工具 6 6 次 272 3,924 新台幣 0.48 元 新台幣 80 元

事前印出的最壞金額是新台幣 10.25 元、預期約 0.86 元,實際兩種情況合計 0.87 元。

工具的成本主要在輸入,三個工具的定義大約 600 個 Token,每一次呼叫模型都要重新送一次,不管這一題用不用得到,觀念題在有工具時輸入是 647 個 Token,沒有工具時只有 43 個,查回來的結果也算輸入,用到三份結果的第四題第二次呼叫的輸入是 1,757 個 Token,所以工具的說明要寫清楚但不要寫成文件,回傳的欄位和列數也要節制。

沒有工具那一側反而比較貴是我沒有預期到的,它的輸入少了三十幾倍,可是思考 Token 用了 3,364 個,有工具時只有 907 個,看起來手上沒有資料的時候模型反而想得比較久,這只是六題各問一次的結果,不能當成定律,但至少說明加上工具不一定比較花錢,費用依 gemini-3.6-flash 在 global 端點的單價(每百萬 Token 輸入 0.75、輸出 3.75 美元)與實際 Token 數算出,匯率以 1 美元約 32 元計,實際以帳單為準。


5. Cloud Shell 實戰演練:把三個工具交給 Gemini

5.1 事前準備

  • 先在 ~/ai-driven-martech-pipeline 執行 git pull,取得 Day 21 的 agent/ 目錄
  • gcloud config get-value project 要印出你的專案 ID
  • 已完成 Day 08(bash attribution/run.sh)、Day 09(bash diagnosis/run.sh)與 Day 17(bash lift/run.sh),今天的三個工具讀的是這三天建好的表,Token 用量表 ops_llm_usage 是 Day 16(bash features/run.sh)建的,也要先有
  • 需要 google-genai 與 google-cloud-bigquery 兩個 Python 套件,缺少時 run.sh 會提示安裝指令

5.2 路線 A|懶人包:一行指令跑完

cd ~/ai-driven-martech-pipeline && git pull && bash agent/run.sh

run.sh 先確認前幾天的表都在、問題和工具定義沒有未 commit 的修改,接著做一次不花錢的預檢,照最長那一題實際查一次表、數一次 Token,確認放得進輸入上限,印出最壞金額與預期金額之後停下來,輸入 yes 才開始逐題問,最後是對答案、10 項檢查和五段報表,全部跑完大約三分鐘。

5.3 路線 B|逐步教學:理解每一個步驟

步驟 1:只看工具,不呼叫模型

cd ~/ai-driven-martech-pipeline/agent
python3 -c "
import json, tools
from google.cloud import bigquery
bq = bigquery.Client(location='US')
print(json.dumps(tools.run_tool(bq, 'get_channel_attribution', {'attribution_model': 'last_touch'})[0], ensure_ascii=False, indent=1))
print(tools.run_tool(bq, 'get_channel_attribution', {'attribution_model': 'linear'})[0])
"

第一行印出六個通路在最終點擊下的功勞,第二行故意給一個不在清單裡的算法,會印出錯誤訊息而且不會查 BigQuery,這一步不花錢,可以先確認工具本身是對的。

步驟 2:問問題

cd ~/ai-driven-martech-pipeline
GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project) python3 agent/ask.py

這一步會呼叫 Gemini,12 題次約新台幣 0.9 元,一樣會先印估價再問,已經問過的題目不會再問。

步驟 3:檢查與報表

bq query --nouse_legacy_sql --format=pretty < agent/check.sql
bq query --nouse_legacy_sql --format=pretty --max_rows=200 < agent/report.sql

5.4 驗證成果

  • check.sql 的 10 項都是 OK,包含 12 題次都有回答、沒有工具那一側的工具呼叫次數是 0、每次輸入都在上限內
  • 報表第 1 段有工具而且需要查資料的四題 tools_ok 與 args_ok 都是 true,第 2 段看得到模型每一次要求的工具與參數
  • 報表第 3 段是 12 則回答的原文,沒有工具那一側編了什麼每次不一定相同,自己讀一遍比看分數有感覺

5.5 用完後怎麼處理

fc_calls_log、fc_tool_log、fc_answers 和 mart_fc_score 四張表都很小,最多的也只有 16 列,留著不會產生費用,明天的助理會沿用 agent/tools.py 的三個工具,ops_llm_usage 裡今天的 16 列 Day 25 做成本儀表板時會用到,都不要刪。


6. 工程實務避坑指南

  1. 工具說明要寫「能回答什麼問題」和「資料到哪裡為止」:完整的工具說明裡列了有哪些通路、資料期間到哪一天,TikTok 那一題模型沒有硬查、日期也補對了年份,這兩樣不寫,模型只能猜
  2. SDK 的自動執行先關掉:開著的時候程式只拿得到最後的回答,模型選了哪個工具、填了什麼參數、查了幾次都不知道,出問題沒辦法查,要記錄用量也少了中間那幾次
  3. 模型上一輪的內容要原樣送回去:只挑函式呼叫的部分自己重組,會少掉思考簽章,下一輪呼叫可能直接回錯誤
  4. 一輪可能有好幾個呼叫:第四題一次回了三個,程式如果只處理第一個,模型會拿著不完整的資料作答,要把這一輪的每個呼叫都執行完再一起送回去
  5. 工具失敗也要回話:參數不對或查詢失敗時把錯誤訊息當成結果回給模型,不要讓程式直接中斷,否則前面幾次呼叫的錢花了卻沒有留下紀錄
  6. 對答案不能只看有沒有出現數字:比對 380 這種數字要整個數字對上,不然 1,380 也會被算成答對,沒有工具那一側更要讀原文,它給的數字格式上完全正常

7. 總結與明日預告

今天把 Day 08 的歸因、Day 09 的異常診斷和 Day 17 的素材成效寫成三個查詢工具交給 Gemini,同樣六個問題,有工具時四題需要查資料的都選對工具、參數正確、數字對得上資料表,不需要查的不查,查不到的說查不到,沒有工具時有四題給了資料表裡沒有的內容,包含前言那筆不存在的 TikTok 花費,12 題次合計約新台幣 0.87 元。

回到篇名,模型會瞎猜不是因為它不夠聰明,是因為它手上沒有資料又被問了需要資料的問題,把查詢的工具交給它之後,數字的來源從模型自己變成資料庫,而工具能查什麼、參數能填什麼、一次能掃多少,都還是寫在我們的程式裡。

明日預告:Day 22《不用寫 SQL,跟 AI 聊天就能查出上週哪支廣告最貴》,今天是一問一答,明天把這三個工具接成可以連續對話的行銷助理,讓它記得前面聊過什麼、接著往下問。


上一篇
Day 20 | 便宜的輕量模型到底夠不夠用?直接實測來看
下一篇
Day 22 | 不用寫 SQL,跟 AI 聊天就能查出上週哪支廣告最貴
系列文
AI-Driven MarTech:用 Google Cloud + Vertex AI 打造全自動廣告歸因與多模態素材分析系統 共 23 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言